Skip to content

feat(desktop): add KLIPY GIF search to composers - #5554

Merged
wpfleger96 merged 21 commits into
mainfrom
kennylopez-klipy-gif-picker
Aug 25, 2026
Merged

feat(desktop): add KLIPY GIF search to composers#5554
wpfleger96 merged 21 commits into
mainfrom
kennylopez-klipy-gif-picker

Conversation

@klopez4212

@klopez4212 klopez4212 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Supersedes Add KLIPY GIF search to message composers #1913 with a KLIPY-hosted URL implementation.
  • Adds KLIPY GIF search and trending results to desktop message and forum composers.
  • Keeps selected GIFs hosted by KLIPY; Buzz stores only the external URL and media metadata (no imeta tag, since relays only accept hash-backed local /media/ entries).
  • Aligns the Emoji/GIF picker with Buzz's standard segmented control, theme surfaces, and motion behavior.

Relay-to-provider boundary

  • The relay proxies KLIPY search/share so the BUZZ_KLIPY_API_KEY never reaches the desktop; the key stays server-side behind a redacted Debug impl.
  • The dedicated GIF reqwest client sets redirect::Policy::none(). Because the API key rides in the request path, following a provider 3xx could replay a key-bearing URL to an attacker-chosen host (an SSRF/key-disclosure primitive). With redirects disabled, a 3xx returns as a non-success status that the handlers map to a generic 502; the Location target is never read or forwarded.
  • Admission reuses the established NIP-98, membership, replay, and per-pubkey rate-limit gates, with an upstream response-size cap and allowlisting so KLIPY error bodies never cross the relay boundary.

Accessibility

  • Under prefers-reduced-motion: reduce, the picker grid renders a static provider poster (a normalized jpg asset) instead of the animated preview, or a named static placeholder when no poster is available. It reacts to preference changes while mounted. no-preference keeps the animated preview.
  • Selected GIFs carry their title through ImetaMedia's displayLabel, so the composer thumbnail, preview dialog, editor, lightbox, and remove control all derive one non-empty accessible name instead of an empty Attachment label. Ordinary hashed uploads keep their existing hash-derived names.

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
…picker

Signed-off-by: kenny lopez <klopez4212@gmail.com>

# Conflicts:
#	desktop/src/features/custom-emoji/ui/EmojiPicker.tsx
#	desktop/src/features/forum/ui/ForumComposer.tsx
Signed-off-by: kenny lopez <klopez4212@gmail.com>

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Thanks for putting this together — the relay proxy is genuinely well built. The key stays server-side with a redacted Debug impl, NIP-98 verification requires the payload hash, and there's a replay guard, membership + per-pubkey quota enforcement, an upstream response-size cap, and allowlisting so KLIPY error bodies never cross the relay boundary. That whole boundary held up under both source review and a live run against the real KLIPY API.

I'm requesting changes on two blocking issues, both verified end-to-end, plus one design question I'd like resolved before merge:

  1. Selecting a GIF produces a message the relay refuses to ingest — the core feature doesn't work end-to-end. A live run against this branch got 400 {"error":"invalid: imeta url must be a local /media/ path"} on both stream (kind 9) and forum (kind 45001) sends, with zero posts persisted. Details inline at desktop/src/features/gifs/api.ts.

  2. The PR breaks its own smoke test: composer-tooltip-dismiss.spec.ts still locates the Insert emoji tooltip, and Desktop Smoke E2E (2) failed that assertion on the initial attempt and both retries. Details inline at ComposerEmojiPicker.tsx.

  3. KLIPY's own reference integrations couple sends to their share/report endpoint, and this PR documents skipping that as a non-goal — I don't think that call is as free as the docs suggest. Inline comment on crates/buzz-relay/src/api/gifs.rs with references.

There are also a few smaller things inline (upstream client reuse, handler-level test coverage, per-relay customer_id keying, and some nits).

Comment thread desktop/src/features/gifs/api.ts
Comment thread desktop/src/features/messages/ui/ComposerEmojiPicker.tsx
Comment thread crates/buzz-relay/src/api/gifs.rs Outdated
Comment thread crates/buzz-relay/src/api/gifs.rs Outdated
Comment thread crates/buzz-relay/src/api/gifs.rs
Comment thread crates/buzz-relay/src/api/gifs.rs Outdated
Comment thread crates/buzz-relay/src/api/gifs.rs Outdated
Comment thread desktop/src/features/gifs/relay.ts Outdated
Comment thread crates/buzz-relay/src/api/gifs.rs Outdated
Comment thread desktop/src/features/gifs/api.ts Outdated
Princess Donut and others added 2 commits August 21, 2026 17:19
Co-authored-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
Co-authored-by: Kenny Lopez <klopez4212@gmail.com>
Signed-off-by: Kenny Lopez <klopez4212@gmail.com>
wpfleger96
wpfleger96 previously approved these changes Aug 21, 2026

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 All feedback from the combined review is addressed at 9db36e2 — verified each finding at source, and both blockers are confirmed fixed: external GIFs now send as content-only markdown (no imeta tag, so the relay validator is never engaged) and the tooltip smoke spec is green. The new /gifs/share endpoint sits behind the same NIP-98/membership/replay boundary as search, and live E2E against the real KLIPY provider confirmed sends persist and render on both kind 9 and forum posts, plus fully graceful degradation (no GIF tab, no errors, zero /gifs/* requests) when the relay has no KLIPY key. CI is fully green at head.

@wpfleger96
wpfleger96 marked this pull request as ready for review August 24, 2026 20:03
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 24, 2026 20:03

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Reviewed: aea0ef8df9fc24d9aa8bf5c761ab2910026a601b..9db36e28afbe76aa3575b0665baa2b316de9d90d (exact head 9db36e28afbe76aa3575b0665baa2b316de9d90d)

Risk: high — this adds a credentialed relay-to-provider HTTP boundary plus externally hosted animated media in shared message/forum composer paths.

Behavior/contracts traced: relay config and NIP-11 capability advertisement; NIP-98/membership/replay/rate-limit admission; KLIPY search/share URL construction and response limits; renderer capability discovery and provider requests; GIF normalization/selection; content-only attachment serialization; shared composer preview/lightbox; forum/message composer reuse; loading/error/empty, motion, and accessibility paths.

Blocking findings

  1. [P1] Provider redirects create an SSRF primitive and can disclose the path-embedded API key. crates/buzz-relay/src/state.rs:858-861 builds the shared reqwest client without a redirect policy. Reqwest follows redirects by default and enables Referer; meanwhile crates/buzz-relay/src/api/gifs.rs:75-105,281-282,317-321 places BUZZ_KLIPY_API_KEY in the request path for both GET search and POST share. A KLIPY 3xx can therefore make the relay connect to an arbitrary redirect target, including loopback/private/link-local services. A cross-origin Referer can carry the prior full key-bearing URL, and 307/308 can replay the share POST body.

    Author action: configure the dedicated GIF client with reqwest::redirect::Policy::none() and treat 3xx as provider failure. Add a causal regression with a redirecting fake upstream and a second listener: assert the second listener receives zero requests and the client-facing error contains neither the API key nor redirect target. Mutation-check the test by restoring default redirects and requiring it to fail.

    Verification owner: author for fix/regression; reviewer for mutation check.

  2. [P1] Reduced-motion users still receive an autoplaying grid of provider animations. desktop/src/features/gifs/ui/KlipyGifPicker.tsx:108-126 renders every preview URL as an ordinary image. The only reduced-motion branch is for the 180 ms tab indicator (desktop/src/features/messages/ui/ComposerEmojiPicker.tsx:44,128-153); there is no static poster, paused state, activation gate, or stop/hide control for the feature's dominant repeating motion. One search can therefore start many indefinite animations simultaneously despite the PR's stated reduced-motion behavior.

    Author action: under prefers-reduced-motion: reduce, render a non-animated poster when the provider supplies one, or a static named placeholder requiring explicit activation before loading animated media. Add deterministic Playwright coverage using page.emulateMedia({ reducedMotion: "reduce" }) proving animated URLs are not loaded/rendered before activation, and mutation-prove the regression.

    Verification owner: author for code/regression; reviewer for exact-head browser/AX inspection.

  3. [P2] GIF selection discards its accessible name in the composer preview and lightbox. The picker has the useful title (KlipyGifPicker.tsx:110-125), but klipyGifAttachment() does not preserve it and sets an empty hash (desktop/src/features/gifs/api.ts:160-169). Shared attachment UI derives all image names from that hash (desktop/src/features/messages/ui/ComposerAttachments.tsx:267,364-368,393-399,427-434), producing indistinguishable Attachment / Attachment preview names for every selected KLIPY GIF.

    Author action: preserve the normalized GIF title on the composer attachment (the existing displayLabel field is suitable), and have the shared attachment preview/lightbox use a non-empty descriptive label while hiding decorative duplicates. Add coverage selecting at least two differently titled GIFs and asserting distinct accessible names in both preview and lightbox.

    Verification owner: author for regression; reviewer for AX-tree/browser verification.

Validation

  • PASS — full Desktop package tests: pnpm test, 5,312 passed / 0 failed, exact head and clean tree rechecked.
  • PASS — git diff --check aea0ef8df9fc24d9aa8bf5c761ab2910026a601b..HEAD.
  • PASS — all required exact-head GitHub checks are green, including Unit Tests, Desktop Core, Desktop smoke/integration, relay integration/E2E, security, cross-compile, and macOS build.
  • ENVIRONMENT FAILURE — cargo test -p buzz-relay: 894 passed, 8 failed, 47 ignored; all eight failures were database-unavailable shapes (PoolTimedOut or dependent status mismatches). Exact-head Unit Tests and relay integration/E2E checks are green, so this is not attributed to the PR.
  • REVIEWER-LOCAL CONFIDENCE GAP — a focused smoke row was 1 pass / 1 fail twice while reusing a port-4173 server; the positive capability row did not observe mocked /info. This conflicts with green exact-head CI and is not classified as another author defect.

Manual/native evidence: no new exact-head native Desktop/AX journey was completed in this review. Source inspection establishes the three defects above independently.

Residual risk: configured-route auth/replay/membership/quota rejection is not directly covered at the handler boundary; source composes the established shared gates correctly. Native keyboard/AX, dark theme, narrow-window, and live forum-send journeys remain unwitnessed. These are confidence gaps, not additional author actions.

— :bot: Jude’s code review agent

Duncan and others added 3 commits August 25, 2026 11:30
…n, a11y names

Resolve the three blocking findings on PR #5554:

- P1 SSRF/key disclosure: the dedicated KLIPY reqwest client followed
  redirects by default while the API key rides in the request path, so a
  provider 3xx could replay a key-bearing URL to an attacker-chosen host.
  build_gif_http_client() now sets redirect::Policy::none(); a 3xx returns
  as a non-success status the handlers map to a generic 502, never reading
  or forwarding Location. Adds a causal regression with a redirecting fake
  upstream and a second listener asserting zero hits and no leak.

- P1 reduced motion: the picker grid autoplayed every animated preview.
  Under prefers-reduced-motion: reduce it now renders a static provider
  poster (a normalized jpg asset) or a named static placeholder, and reacts
  to preference changes while mounted.

- P2 accessibility: KLIPY GIFs dropped their title, leaving empty
  'Attachment ' names in the composer preview and lightbox. displayLabel now
  carries the title through BlobDescriptor; MediaAttachmentItem derives one
  non-empty accessible name for the thumbnail, dialog, editor, lightbox, and
  remove control.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker

* origin/main: (57 commits)
  Add staging dev relay image workflow (#6709)
  Extract community persistence (#6668)
  Fix mobile Huddle agent voice turn states (#6611)
  Add inline profile camera capture (#6680)
  Hide Huddles in mobile agent DMs (#6676)
  fix(desktop): polish inline chip states (#6718)
  Centralize replaceable event persistence (#6660)
  feat(workflows): discover trigger filter values (#6712)
  feat(desktop): simplify the message action rail (#6529)
  fix(desktop): restore icon-only remote marker (#6491)
  fix(ci): prevent poisoned Rust caches (#6618)
  docs(security): route reports through private advisories (#6728)
  fix(composer): wrap Buzz chip labels without orphaning icons (#6581)
  fix(desktop): bound thread /query and surface load errors, not false-empty (#6447)
  fix(messages): route edits to the owning composer (#6575)
  fix(mobile): join starter channels after accepting invite (#5915)
  Add mobile profile editing (#6583)
  fix(desktop): align jump-to-latest pill with composer height (#6606)
  fix(desktop): emit singular `mention` feed category so alerts route correctly (#6665)
  fix(mobile): recover stale and shuffled messages (#6691)
  ...

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The desktop file-size ratchet freezes tauri.ts and MessageComposer.tsx,
both already over the 1000-line ceiling on main. Carry the KLIPY GIF
title on the existing ImetaMedia type (which already declares the
composer-only displayLabel) instead of widening the frozen BlobDescriptor,
and widen MediaAttachmentItem's prop to ImetaMedia to read it. Collapse one
useMentions call to a single line so the merge's essential
gifMediaController wiring keeps MessageComposer.tsx at its frozen size.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 changed the title Add KLIPY GIF search to desktop composers feat(desktop): add KLIPY GIF search to composers Aug 25, 2026
Duncan and others added 10 commits August 25, 2026 12:21
The accessible-name fallback preferred filename over the historical hash
label, so ordinary image/video uploads (which carry a filename) lost their
'Attachment <hash>' / 'Video attachment <hash>' names — breaking the exact
assertions in composer-image-draw, video-attachment, and spoiler specs.
Prefer displayLabel, then the type-aware hash label when sha256 is present,
and only fall back to filename for genuinely hashless non-provider media.
Make the lightbox view image decorative since the dialog title already names
it, removing the duplicate accessible name.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker

* origin/main:
  highlight search terms in results and messages (#6702)
  fix(desktop): make lightbox zoom controls interactive (#6710)
  Support community deletion in versioned media buckets (#6738)
  Fix TipTap editor mount race (#6779)
  feat(buzz-agent): gate LLM tool calls on session/request_permission (#5712)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Radix Trigger with asChild on a bare div left the interactive element
role-less, so the composer attachment had no reliable single accessible
name (and the video-without-poster branch had none at all). Render the
trigger as a real button carrying aria-label={mediaLabel}, make both
thumbnail images decorative, and locate the trigger by role/name in the
composer, spoiler, video, and GIF specs.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
openGifGrid() already opens the emoji/GIF popover, but the selection
loop's first iteration re-clicked composer-emoji-button — the Radix
PopoverTrigger — toggling the just-opened popover closed. The GIFs-tab
click then raced the popover's exit/unmount, so Playwright saw the
target go unstable then detach and timed out (deterministic across all
retries). The popper position itself never moved.

Drop the redundant openGifGrid() preamble so each loop iteration owns
the single open→select cycle it expects. The other three picker tests
keep using openGifGrid() unchanged.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker

* origin/main:
  Qualify canonical relay images for staged delivery (#6781)
  feat(desktop): persist agent addressing across composer messages (#6714)
  feat: navigate images across message threads (#6705)
  Add database pressure observability (#6700)
  revert fixed mention highlight (#6716)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>

# Conflicts:
#	desktop/src/features/messages/ui/MessageComposer.tsx
…picker

* origin/main:
  docs(nest): make commit attribution policy-neutral (#6707)
  fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)

Signed-off-by: Hayt <9e1c23a3fd83f61da34420e4e88ff1b16e45cafcc0cd9019eb07d4ecfa8ca9b0@buzz.block.builderlab.xyz>
Main #6793 grew MessageComposer.tsx to the 1000-line ceiling; the branch's
gifMediaController prop tipped it one line over the ratchet. Drop the
redundant trackDraftAuthoredContent destructure alias (single call site) to
reclaim the line without touching the ratchet config or behavior.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker

* origin/main:
  Remove public relay signing key fallback (#6729)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
The error toast raised when agent startup fails renders bottom-right,
overlapping the send button. The first send leaves the pointer parked on
the toast, which pauses sonner's auto-dismiss timer, so the retry click is
intercepted for the full timeout. Move the cursor off the toast and wait
for it to clear before retrying.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…picker

* origin/main:
  chore(release): release Buzz Desktop version 0.5.19 (#6828)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
@wpfleger96
wpfleger96 merged commit 8471049 into main Aug 25, 2026
35 checks passed
@wpfleger96
wpfleger96 deleted the kennylopez-klipy-gif-picker branch August 25, 2026 23:09
wpfleger96 pushed a commit that referenced this pull request Aug 25, 2026
…-history

* origin/main:
  feat(desktop): add KLIPY GIF search to composers (#5554)
  fix(desktop): respect automatic mention preference after send (#6837)
  fix(release): attribute desktop candidates to the operator (#6831)
  fix(ci): check out source in docker.yml merge job (#6833)
  chore(release): release Buzz Desktop version 0.5.19 (#6828)
  Remove public relay signing key fallback (#6729)
  docs(nest): make commit attribution policy-neutral (#6707)
  fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
salman1993 added a commit that referenced this pull request Aug 26, 2026
…cp-sessions

* origin/main: (31 commits)
  fix(desktop): stop pulsing addressed agents on send (#6873)
  fix(desktop): prioritize sidebar channel status (#6861)
  feat(desktop): hyperlink selected composer text on link paste (#6684)
  chore(release): release Buzz Desktop version 0.5.20 (#6839)
  feat(desktop): add KLIPY GIF search to composers (#5554)
  fix(desktop): respect automatic mention preference after send (#6837)
  fix(release): attribute desktop candidates to the operator (#6831)
  fix(ci): check out source in docker.yml merge job (#6833)
  chore(release): release Buzz Desktop version 0.5.19 (#6828)
  Remove public relay signing key fallback (#6729)
  docs(nest): make commit attribution policy-neutral (#6707)
  fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)
  Qualify canonical relay images for staged delivery (#6781)
  feat(desktop): persist agent addressing across composer messages (#6714)
  feat: navigate images across message threads (#6705)
  Add database pressure observability (#6700)
  revert fixed mention highlight (#6716)
  highlight search terms in results and messages (#6702)
  fix(desktop): make lightbox zoom controls interactive (#6710)
  Support community deletion in versioned media buckets (#6738)
  ...

Signed-off-by: Salman Mohammed <smohammed@squareup.com>
wpfleger96 pushed a commit that referenced this pull request Aug 26, 2026
* origin/main:
  Use paired tags for standing & per-turn context (#6701)
  fix(cli): preserve signatures in event reads (#6884)
  refactor(db): finish replaceable event store extraction (#6777)
  Fix Admin feedback filter overflow (#6825)
  fix(desktop): stop pulsing addressed agents on send (#6873)
  fix(desktop): prioritize sidebar channel status (#6861)
  feat(desktop): hyperlink selected composer text on link paste (#6684)
  chore(release): release Buzz Desktop version 0.5.20 (#6839)
  feat(desktop): add KLIPY GIF search to composers (#5554)
  fix(desktop): respect automatic mention preference after send (#6837)
  fix(release): attribute desktop candidates to the operator (#6831)
  fix(ci): check out source in docker.yml merge job (#6833)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
brow added a commit that referenced this pull request Aug 26, 2026
…ignment

* origin/main: (33 commits)
  feat(desktop): restore message quick reactions (#6892)
  Use paired tags for standing & per-turn context (#6701)
  fix(cli): preserve signatures in event reads (#6884)
  refactor(db): finish replaceable event store extraction (#6777)
  Fix Admin feedback filter overflow (#6825)
  fix(desktop): stop pulsing addressed agents on send (#6873)
  fix(desktop): prioritize sidebar channel status (#6861)
  feat(desktop): hyperlink selected composer text on link paste (#6684)
  chore(release): release Buzz Desktop version 0.5.20 (#6839)
  feat(desktop): add KLIPY GIF search to composers (#5554)
  fix(desktop): respect automatic mention preference after send (#6837)
  fix(release): attribute desktop candidates to the operator (#6831)
  fix(ci): check out source in docker.yml merge job (#6833)
  chore(release): release Buzz Desktop version 0.5.19 (#6828)
  Remove public relay signing key fallback (#6729)
  docs(nest): make commit attribution policy-neutral (#6707)
  fix(desktop-messages): preserve inline agent mentions with persistent addressing (#6793)
  Qualify canonical relay images for staged delivery (#6781)
  feat(desktop): persist agent addressing across composer messages (#6714)
  feat: navigate images across message threads (#6705)
  ...

Signed-off-by: Tom Brow <tomb@block.xyz>

# Conflicts:
#	.env.example
#	crates/buzz-db/src/push.rs
#	mobile/lib/shared/profile/user_cache_provider.dart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants